Spring Boot 概述及自动装配原理(基于3.x更新)

spring boot 简介

SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用(例如微服务应用,详细参照 微服务详细介绍(中文)微服务详细介绍(英文)),可以说springboot是框架中的框架,大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术。其实可以使用单反相机和傻瓜相机来类比spring 和 springboot 的关系,一个springboot项目你只需要简单地 ”按一下快门“ 就能快速构建出来。如果需要深度定制,则需要深度理解springboot的自动装配原理。


快速搭建一个应用

首先我们来感受一下sb构建一个应用到底有多简单。参考 文档

1) 创建maven工程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.9</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>demo-springboot</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!--导入场景Starter:spring-boot-starter-web,自动配置生效的原理简介。-->
<!--场景启动器除了会导入相关功能依赖,最重要的是最终都导入一个 spring-boot-starter ,
它是所有 starter 的 starter ,基础核心 starter。spring-boot-starter 导入了
spring-boot-autoconfigure 。包里面包括全场景的 AutoConfiguration 自动配置类。
虽然全场景的AutoConfiguration自动配置都在autoconfigure包,但不是全都开启的。它是
按需加载的,导入了哪个 starter 哪个 starter 才会生效。-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

2) 配置文件 src/main/resources/application.properties(或者ymal)

配置文件的所有配置项是和某个类的对象值进行一一绑定的。绑定了配置文件中每一项值的类被称为属性类。spring 官方提供了一系列支持的参数和属性类,例如ServerProperties 绑定了所有Tomcat服务器有关的配置,MultipartProperties 绑定了所有文件上传相关的配置,这部分可以参考:官方提供的 Application Properties官方提供的 Server Properties

1
2
server:
port: 8081

3) 编写启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

//@ImportResource(locations = {"classpath:beans.xml"})
@SpringBootApplication
public class App {
// @SpringBootApplication 标注的类就是主程序类
// SpringBoot只会扫描主程序所在的包及其下面的子包,自动的component-scan功能
// 自定义扫描路径1:@SpringBootApplication(scanBasePackages = "com.atguigu")
// 自定义扫描路径2:@ComponentScan("com.atguigu") 直接指定扫描的路径
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}}

4) 编写相关的Controller、Service

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloController {
@ResponseBody
@RequestMapping("/hello")
public String hello() {
return "hello world";
}
}

5) 开发环境启动应用

1
2
# 直接使用 eclipse 或 idea 工具启动主程序类
$ curl http://localhost:8080/hello

6) 部署应用

这个maven插件可以将应用打包成一个可执行的jar包,参考 文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

打包部署和运行项目

1
2
3
$ mvn clean package -Dmaven.test.skip=true 
$ java -jar xxx.jar
$ curl http://localhost:8080/hello

[新增] 关于使用 maven-assembly-plugin 插件组织项目结构,请参考 使用-assembly-打生产包


SB核心原理-自动装配

请读者自己思考一下:SpringBoot怎么实现导一个 starter 、写些简单配置,应用就能跑起来?为什么Tomcat的端口号可以配置在 application.properties 中,并且 Tomcat 能启动成功?

自动配置流程细节梳理

第一:根据业务导入需要的场景 starter,比如 spring-boot-starter-web

  • 场景启动器导入了相关场景的所有依赖: starter-json 、 starter-tomcat 、 springmvc
  • 几乎每个场景启动器都引入了 spring-boot-starter,它是SB的核心场景启动器(它是starter中的starter)。
  • 核心场景启动器引入了 spring-boot-autoconfigure 包,其中囊括了几乎所有场景的所有配置。
  • 只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了。
  • SB默认只扫描主程序及其子孙路径下的组件,是扫不到 spring-boot-autoconfigure 下写好的所有配置类的。

第二,从主程序 @SpringBootApplication 来分析

  • @SpringBootApplication 由三个注解组成 @SpringBootConfiguration 、 @EnableAutoConfiguration、 @ComponentScan。
  • SpringBoot 默认只能扫描自己主程序所在的包及其下面的子孙包,扫描不到 spring-boot-autoconfigure 包中官方写好的配置类。
  • @EnableAutoConfiguration 是 SpringBoot 开启自动配置的核心。它由 @Import(AutoConfigurationImportSelector.class) 提供功能,批量给容器中导入组件。项目启动的时候SB 会利用 @Import 批量导入组件机制把 spring-boot-autoconfigure 包下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件指定的100多个 XxxAutoConfiguration 类导入进来。这100多个 XxxAutoConfiguration 类虽然被导入了,但是这个配置中定义的 Bean 基本都是被 @ConditionalOnXxx 注解的,只有满足条件的Bean才能注册到spring工厂,本质上它们都是按需加载。

第三,我们再来看这些被加载的 XxxAutoConfiguration 自动配置类

  • 这些配置类 XxxAutoConfiguration 基本都是用 @Bean 给容器中放一堆相关的组件。
  • 每个自动配置类都可能使用注解 @EnableConfigurationProperties(XxxProperties.class) ,把配置文件中配的指定前缀的属性值封装到 XxxProperties 属性类中。以Tomcat为例,对应的属性配置类是 ServerProperties(该类被 @ConfigurationProperties(“server”) 修饰 ),其配置都是以 server 开头的,这些属性值都被封装在了 ServerProperties 中。这些 XxxProperties 完成了配置文件的参数和应用组件的绑定。
  • 开发者只需要改配置文件的值,核心组件的底层参数都能修改,全程无需关心各种整合(底层这些整合写好了,而且也生效了)。


使用 starter 的最佳姿势

第一,选择所需的场景starter,导入到项目

  • 官方 starter,命名一般是 spring-boot-starter-xxx,可以参考 springboot starters
  • 第三方的 stater,命名一般是 xxx-spring-boot-starter

第二,根据 XxxAutoConfiguration 的 XxxProperties 写配置,改配置文件关键项

  • 例如数据库参数(连接地址、账号密码…)

第三,根据 XxxAutoConfiguration 分析这个starter场景给我们导入了哪些能用的组件

  • 自动装配这些组件进行后续使用
  • 不满意boot提供的自动配好的默认组件可以进行深度定制化(改配置或者自定义组件)


以整合redis为例:

  • 选场景: spring-boot-starter-data-redis,场景 RedisAutoConfiguration 就是这个场景的自动配置类
  • 写配置:
    • 分析自动配置类开启了哪些属性绑定关系 @EnableConfigurationProperties(RedisProperties.class)
    • 根据这个绑定的属性配置类来修改redis相关的配置
  • 分析组件:
    • 分析到 RedisAutoConfiguration 给容器中放了哪些bean,如 StringRedisTemplate
    • 给业务代码中自动装配 StringRedisTemplate
  • 定制化:
    • 修改配置文件
    • 自定义组件,自己给容器中放一个 StringRedisTemplate


常用注解

组件注册注解

  • @SpringBootApplication、@ImportResource(locations = {“classpath:x.xml”})、@Import(XComponent.class)
  • @Configuration(≈@SpringBootConfiguration)、@Bean、@Scope、@ComponentScan
  • @Controller、 @Service、@Repository、@Component


组件注册的条件注解

@ConditionalOnXxx :如果注解指定的条件成立,则触发指定行为。如果放在方法上,则单独判断这个方法;如果放在类上,则是 判断这个配置配是否生效。

@ConditionalOnClass:如果类路径中存在这个类,则触发指定行为
@ConditionalOnMissingClass:如果类路径中不存在这个类,则触发指定行为
@ConditionalOnBean:如果容器中存在这个Bean(组件),则触发指定行为
@ConditionalOnBean(value=组件类型,name=组件名字):判断容器中是否有这个类型的组件,并且名字是指定的值
@ConditionalOnMissingBean:如果容器中不存在这个Bean(组件),则触发指定行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 检查类路径Classpath下是否存在某个类,或者某个文件资源。
@ConditionalOnClass // 当类路径下存在指定的类时才生效。常用于集成第三方库(如 Redis 存在才配置连接池)。
@ConditionalOnMissingClass // 当类路径下不存在指定的类时才生效。
@ConditionalOnResource // 当指定的 资源文件(如 config.xml)存在于类路径时生效。

// 检查容器中 Bean 的存在情况,防止重复定义或依赖缺失。
@ConditionalOnBean // 当容器中已存在指定的 Bean 时才创建当前 Bean。用于处理 Bean 之间的依赖顺序。
@ConditionalOnMissingBean // 不存在指定Bean才创建。这允许通过自定义Bean来覆盖Spring的默认配置。
@ConditionalOnSingleCandidate // 当指定的Bean在容器中只有一个实例,或有多个但指定了首选时生效。
@ConditionalOnMissingFilterBean // 专门用于 Web 场景,检查容器中是否缺失某个特定的 Filter Bean。

// 通过 application.properties/yml 中的配置来开关功能。
@ConditionalOnProperty // 检查配置文件中是否有某个属性,且值是否匹配。例如 @ConditionalOnProperty(name="owlias.enable", havingValue="true")。
@ConditionalOnExpression // 使用 SpEL 表达式 来决定是否加载。最灵活但也最复杂。
@Profile // 属于 Spring Core。根据当前激活的环境(如 dev, test, prod)来加载。

// 判断当前应用运行的类型(Servlet、Reactive)或环境。
@ConditionalOnWebApplication // 当应用是 Web 应用(基于 Servlet 或 Reactive)时生效。
@ConditionalOnNotWebApplication // 当应用 不是 Web 应用时生效(如纯命令行工具)。
@ConditionalOnWarDeployment // 当应用是以 WAR 包 形式部署到外部服务器(如 Tomcat)时生效。
@ConditionalOnDefaultWebSecurity // Spring Security 相关。当用户没有配置自己的安全策略时,加载默认的安全配置。

// 针对特定的运行时环境、云平台或数据库类型。
@ConditionalOnJava // 检查当前的 Java 版本(如必须是 Java 17 以上)。
@ConditionalOnCloudPlatform // 检查是否运行在特定的 云平台(如 AWS, Heroku, Cloud Foundry)。
@ConditionalOnJndi // 检查指定的 JNDI(Java 命名和目录接口)是否存在。
@ConditionalOnRepositoryType // Spring Data 相关。根据数据仓库的类型(如 Imperative 或 Reactive)来决定。

// 针对特定技术栈(如 GraphQL 或 DevTools)。
@ConditionalOnGraphQlSchema // 只有当定义的 GraphQL Schema 准备就绪时才生效。
@ConditionalOnEnabledResourceChain // 用于控制 Web 资源链(Resource Chain)是否启用。
@ConditionalOnInitializedRestarter // DevTools 专用。 只有当应用是通过 DevTools 的 Restarter 重启启动时生效。


属性绑定注解

例如下面配置文件 src/main/resources/person.properties

1
2
3
4
5
6
7
8
9
10
11
12
person.last-name=zhang\nsan
person.department=yan\\nfa\\nbu
person.age=23
person.birhtday=2019/01/01 12:12:12
person.boss=true
person.map.k1=v1
person.map.k2=v2
person.list[0]=0
person.list[1]=1
person.list[2]=2
person.dog.name=xiaohei
person.dog.age=${random.int[10,20]}

或 person.ymal 文件形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 细节
## 文本:单引号不会转义【\n 则为普通字符串显示】,双引号会转义【\n会显示为换行符】
## 大文本:| 开头,大文本写在下层,保留文本格式,换行符正确显示;> 开头,大文本写在下层,折叠换行符
## 多文档合并:使用 --- 可以把多个yaml文档合并在一个文档中,每个文档区依然认为内容独立
person:
last-name: "zhang\nsan" #默认”-“和驼峰可以被等价解析
department: "yan\\nfa\\nbu"
age: 23
birhtday: 2019/01/01 12:12:12
boss: true
map: #也可以表示为 {k1:v1, k2:v2} 的形式
k1: v1
k2: v2
list: #也可以表示为 [0,1,2] 的形式
- 0
- 1
- 2
dog:
name: xiaohei
age: ${random.int[10,20]}

对应的属性配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@Component
@ConfigurationProperties(prefix = "person")
@PropertySource(value = {"classpath:person.properties"}) // 不写默认从已加载配置文件中匹配
public class Person {
private String lastName;
private Integer age;
private Boolean boss;
private Date birth;

private Map<String,Object> map;
private List<Object> list;
private Dog dog;
}

@Data
public class Dog {
private String name;
private Integer age;
}
1
2
3
4
5
6
7
8
9
@Component
public class Person2 {
@Value("${person.last-name}")
private String lastName;
@Value("#{11*2}") // 支持SpEL表达式
private Integer age;
@Value("true")
private Boolean boss;
}

如果上述Person类没有标注 @Component 或使用 @Bean new 对象将其放到容器,也可以在配置类上标注 @EnableConfigurationProperties({Person.class, Person2.class}) 将其注入到容器中。@EnableConfigurationProperties 典型的场景是用于第三方组件的属性绑定。


日志配置

Spring使用commons-logging作为内部日志,但底层日志实现是开放的,也可对接其他日志框架。spring5及以后 commons-logging 被 spring直接封装了。Spring 支持 jul、log4j2、logback,提供了默认的控制台输出配置,也可以配置输出为文件。虽然日志框架很多,但是我们不用担心,使用 SpringBoot 的默认配置就能工作的很好。


日志的自动装配

  • 每个 starter 场景,都会导入一个核心场景 spring-boot-starter
  • 核心场景引入了日志的所用功能 spring-boot-starter-logging
  • 默认使用了 logback + slf4j 组合作为默认底层日志
  • 日志是系统一启动就要用 , 而 XxxAutoConfiguration 是系统启动好了以后放好的组件,所以日志stater是不存在 XxxAutoConfiguration 的。
  • 日志是利用监听器机制配置好的,ApplicationListener 。
  • 日志所有的配置都可以通过修改配置文件实现,它是以 logging 开始的所有配置。


日志依赖的切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>


日志的全局配置

日志分组:将相关的 logger 分组在一起,统一配置。比如:Tomcat 相关的日志统一设置。

1
2
3
4
5
6
7
8
9
10
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace

# SpringBoot 预定义两个组
# web
## org.springframework.core.codec, org.springframework.http,
## org.springframework.web, org.springframework.boot.actuate.endpoint.web,
## org.springframework.boot.web.servlet.ServletContextInitializerBeans
# sql
## sql org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

日志级别:由低到高 ALL, TRACE, DEBUG, INFO, WARN, ERROR ,FATAL, OFF。不指定级别的所有类,都使用root指定的级别作为默认级别。SpringBoot 日志默认级别是 INFO。

1
2
3
# 在application.properties/yaml中配置
# root 的logger-name叫root,可以配置logging.level.root=warn
logging.level.<logger-name>=<level>

文件归档与滚动切割:

  • 归档:每天的日志单独存到一个档中。
  • 切割:每个文件10MB,超过切割成另外个件。

  • logging.file.name 和 logging.file.path:如果两者均未指定则仅仅控制台输出;若都指定则以 logging.file.name 为准。

  • logging.logback.rollingpolicy.file-namepattern:日志存档的文件名格式(默认值: ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz)。
  • logging.logback.rollingpolicy.clean-historyon-start:应用启动时是否清除以前存档(默认值: false)。
  • logging.logback.rollingpolicy.max-file-size:存档前,每个日志文件的最大大小(默认值: 10MB)。
  • logging.logback.rollingpolicy.total-size-cap:日志文件被删除之前,可以容纳的最大大小(默认值:0B)。设置1GB则磁盘存储超过 1GB 日志后就会删除旧日志文件。
  • logging.logback.rollingpolicy.max-history:日志文件保存的最大天数(默认值:7)。

我们建议的最佳实战是:自己要写配置,配置文件名加上 xx-spring.xml:

  • Logback:logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
  • Log4j2:log4j2-spring.xml or log4j2.xml
  • JDK (Java Util Logging):logging.properties

如果可能,我们建议您在日志配置中使用 -spring 变量(例如, logback-spring.xml 而不是 logback.xml )。如果您使用标准配置文件,spring 无法完全控制日志初始化。


日志的最佳实战

  • 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志。
  • 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如 logback-spring.xml , log4j2-spring.xml。
  • 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka 之类的中间件,这和 SpringBoot 没关系,都是日志框架自己的配置,修改配置文件即可。
  • 业务中使用slf4j-api记录日志。不要再 sout 了。